﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Expect.Core.Errors;
using Expect.Core.Interfaces;

namespace Expect.Core
{
    /// <summary>
    /// A set of helper methods for using reflection
    /// </summary>
    public static class ReflectionHelper
    {
        /// <summary>
        /// Set the value of a property
        /// </summary>
        /// <param name="catalog"></param>
        /// <param name="target"></param>
        /// <param name="propName"></param>
        /// <param name="expected"></param>
        /// <param name="textval"></param>
        /// <returns></returns>
        public static bool SetPropertyValue(this IExpectCatalog catalog, object target, string propName, object expected, string textval)
        {
            PropertyInfo property = target.GetType().GetProperty(propName);
            if (property == null)
            {
                return false;
            }
            
            if (expected == null)
            {
                expected = catalog.GetValue(property.PropertyType, textval);
            }
            if (expected.GetType() != property.PropertyType)
            {
                expected = catalog.GetValue(property.PropertyType, textval);
            }

            property.SetValue(target, expected, null);
            
            return true;
        }

        /// <summary>
        /// Set the value of a field
        /// </summary>
        /// <param name="catalog"></param>
        /// <param name="target"></param>
        /// <param name="propName"></param>
        /// <param name="expected"></param>
        /// <param name="textval"></param>
        /// <returns></returns>
        public static bool SetFieldValue(this IExpectCatalog catalog, object target, string propName, object expected, string textval)
        {
            FieldInfo field = target.GetType().GetField(propName);
            
            if (field == null)
            {
                return false;
            }

            if (expected == null)
            {
                expected = catalog.GetValue(field.FieldType, textval);
            }
            if (expected.GetType() != field.FieldType)
            {
                expected = catalog.GetValue(field.FieldType, textval);
            }

            field.SetValue(target, expected);

            return true;
        }


        
        /// <summary>
        /// Invoke a method on the target
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="target"></param>
        /// <param name="methodName"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public static object RunMethod(this IExecutionContext ctx, object target, string methodName, string[] args)
        {
            return RunMethod(ctx, ctx.GetMethod(target.GetType(), methodName), target, args, null);
        }

        /// <summary>
        /// Invoke a method on the target
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="target"></param>
        /// <param name="methodName"></param>
        /// <param name="args"></param>
        /// <param name="data"></param>
        /// <returns></returns>
        public static object RunMethod(this IExecutionContext ctx, object target, string methodName, string[] args, FixtureData data)
        {
            return RunMethod(ctx, ctx.GetMethod(target.GetType(), methodName), target, args, data);
        }

        /// <summary>
        /// Invoke a method on the target
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="method"></param>
        /// <param name="target"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public static object RunMethod(this IExecutionContext ctx, MethodInfo method, object target, string[] args)
        {
            return RunMethod(ctx, method, target, args, null);
        }

        /// <summary>
        /// Get instance of a type with arguments
        /// </summary>
        /// <param name="catalog"></param>
        /// <param name="targetType"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public static object GetInstance(this IExpectCatalog catalog, Type targetType, string[] args)
        {
            var constructors = targetType.GetConstructors();
            List<string> resargs = new List<string>();
            if (args != null)
            {
                foreach (var r in args)
                {
                    if (!string.IsNullOrEmpty(r))
                    {
                        resargs.Add(r);
                    }
                }
            }

            foreach (var ctor in constructors)
            {
                var parametersDef = ctor.GetParameters();
                if (parametersDef.Length == resargs.Count)
                {
                    var parameters = GetParameters(catalog, parametersDef, resargs.ToArray(), null);
                    return ctor.Invoke(parameters);
                }
            }            

            return null;
        }

        /// <summary>
        /// Invoke a method on the target
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="method"></param>
        /// <param name="target"></param>
        /// <param name="args"></param>
        /// <param name="data"></param>
        /// <returns></returns>
        public static object RunMethod(this IExecutionContext ctx, MethodInfo method, object target, string[] args, FixtureData data)
        {
            object[] parameters = GetParameters(ctx.Catalog, method.GetParameters(), args, data);

            object res = null;

            try
            {
                res = method.Invoke(target, parameters);
            }
            catch (TargetInvocationException ex)
            {
                ex.InnerException.PreserveStackTrace();

                throw ex.InnerException;
            }
            
            return res;
        }

        private static object[] GetParameters(IExpectCatalog catalog, ParameterInfo[] parametersDef, string[] values, FixtureData data)
        {
            object[] parameters = new object[0];
            if ((values != null && values.Length > 0) || data != null)
            {
                int numParams = 0;
                
                if (values != null)
                    numParams = values.Length;

                if (data != null)
                {
                    numParams = numParams + 1;
                }
                parameters = new object[numParams];
                
                for (int i = 0; i < values.Length; i++)
                {
                    parameters[i] = catalog.GetValue(parametersDef[i].ParameterType, values[i]);
                }
                
                if (data != null)
                {
                    parameters[parameters.Length - 1] = data;
                }
            }
            return parameters;
        }

        /// <summary>
        /// Invoke a method
        /// </summary>
        /// <param name="Context"></param>
        /// <param name="methodOwner"></param>
        /// <param name="cellReport"></param>
        /// <param name="cellvalue"></param>
        /// <param name="methodname"></param>
        /// <param name="checkReturn"></param>
        /// <param name="inputs"></param>
        /// <returns></returns>
        public static bool RunMethod(IExecutionContext Context, object methodOwner, FixtureCell cellReport, FixtureCell cellvalue, string methodname, bool checkReturn, string[] inputs)
        {
            string conformedMethodname = Context.NameProvider.ElementName(methodname);
            cellvalue.ActualValue = Context.RunMethod(methodOwner, conformedMethodname, inputs);
            if (checkReturn)
                return CompareCellValues(cellReport, cellvalue);

            return true;
        }

        /// <summary>
        /// Compare ActualValue and ExpectedValue of the cell. 
        /// If ExpectedValue is null, it tries to fill it from the text of the cell
        /// </summary>
        public static bool CompareCellValues(FixtureCell cellReport, FixtureCell cellvalue)
        {
            if (cellvalue.ExpectedValue == null)
            {
                cellvalue.ExpectedValue = Convert.ChangeType(cellvalue.Text, cellvalue.ActualValue.GetType());
            }
            else
            {
                if (cellvalue.ExpectedValue.GetType() != cellvalue.ActualValue.GetType())
                {
                    cellvalue.ExpectedValue = Convert.ChangeType(cellvalue.ExpectedValue, cellvalue.ActualValue.GetType());
                }
            }

            if (!cellvalue.ExpectedValue.Equals(cellvalue.ActualValue))
            {
                cellReport.Status.Success = false;
                cellReport.Status.AddError(new ValueMismatchError(cellvalue.ExpectedValue, cellvalue.ActualValue));
            }
            else
            {
                cellReport.Status.Success = true;
            }
            return cellReport.Status.Success.Value;
        }

        /// <summary>
        /// Set a field or property value
        /// </summary>
        /// <param name="context"></param>
        /// <param name="fixture"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        public static bool HasFieldOrProperty(this IExecutionContext context, IFixtureImplementation fixture, string name)
        {
            string conformed = context.NameProvider.ElementName(name);
            var fixtureType = fixture.GetType();
            if (fixtureType.GetProperty(conformed) != null || fixtureType.GetField(conformed) != null)
                return true;

            return false;
        }

        /// <summary>
        /// Set a field or property value
        /// </summary>
        /// <param name="context"></param>
        /// <param name="fixture"></param>
        /// <param name="cell"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        public static bool? SetFieldOrProperty(this IExecutionContext context, IFixtureImplementation fixture, FixtureCell cell, string name)
        {
            string conformed = context.NameProvider.ElementName(name);
            if (SetObjectFieldOrProperty(context, fixture, conformed, cell.ExpectedValue, cell.Text)) 
                return true;
           
            fixture.Fixture.Status.AddError(
                    ElementNotFoundError.PropertyOrField(conformed, fixture.GetType().FullName));
                return false;
            
        }

        /// <summary>
        /// Set a field or property value
        /// </summary>
        /// <param name="context"></param>
        /// <param name="target"></param>
        /// <param name="conformed"></param>
        /// <param name="expected"></param>
        /// <param name="textval"></param>
        /// <returns></returns>
        public static bool SetObjectFieldOrProperty(this IExecutionContext context, object target, string conformed, object expected, string textval)
        {
            try
            {
                if (ReflectionHelper.SetPropertyValue(context.Catalog, target, conformed, expected, textval))
                    return true;
                if (ReflectionHelper.SetFieldValue(context.Catalog, target, conformed, expected, textval))
                    return true;
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException("Expect could not resolve field or property " + conformed + " with value '" + textval + "' on class " + target.GetType().FullName, ex);
            }

            return false;
        }

        /// <summary>
        /// Indicate if the target type has a method named after the input
        /// </summary>
        /// <param name="context"></param>
        /// <param name="targetType"></param>
        /// <param name="input"></param>
        /// <param name="numArgs"></param>
        /// <returns></returns>
        public static bool HasMethod(this IExecutionContext context, Type targetType, string input, int numArgs)
        {
            string conformed = context.NameProvider.ElementName(input);
            var meth = context.GetMethod(targetType, conformed, false);
            
            if(meth == null) return false;
            
            var parameters = meth.GetParameters();
            if (parameters.Length == numArgs && !parameters.Any(p => p.ParameterType == typeof(FixtureData)))
                return true;

            return false;
        }

        /// <summary>
        /// Indicate if the target type has a method named after the input
        /// </summary>
        /// <param name="Context"></param>
        /// <param name="targetType"></param>
        /// <param name="input"></param>
        /// <param name="numArgs"></param>
        /// <returns></returns>
        public static bool HasMethodWithFixtureData(IExecutionContext Context, Type targetType, string input, int numArgs)
        {
            string conformed = Context.NameProvider.ElementName(input);
            var meth = Context.GetMethodWithFixtureData(targetType, conformed, numArgs);

            if (meth != null) return true;

            return false;
        }

        /// <summary>
        /// List all maps to a method for a type
        /// </summary>
        /// <param name="context"></param>
        /// <param name="targetType"></param>
        /// <returns></returns>
        public static Dictionary<string, MethodInfo> GetMethodsFor(this IExecutionContext context, Type targetType)
        {
            Dictionary<string, MethodInfo> res = new Dictionary<string, MethodInfo>();
            return res;
        }

        private static IEnumerable<MethodInfo> MethodsFor(this IExecutionContext context, Type targetType, string methodName)
        {
            var methods = targetType.GetMethods();
            foreach (var methodInfo in methods)
            {
                if (string.Compare(methodName, methodInfo.Name, true) == 0) 
                    yield return methodInfo;

                var attrs = methodInfo.GetCustomAttributes(typeof(FixtureHintAttribute), true);
                if (attrs != null && attrs.Length > 0)
                {
                    foreach (FixtureHintAttribute attr in attrs)
                    {
                        var tokens = attr.Name.Split('|');
                        FixtureRow row = new FixtureRow(tokens);
                        var elt = context.NameProvider.ElementName(row.NamesForFluent().ToArray());
                        if (string.Compare(elt, methodName, true) == 0) 
                            yield return methodInfo;
                    }
                }
            }
        }

        /// <summary>
        /// Get the named method from a type
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="t"></param>
        /// <param name="methodName"></param>
        /// <returns></returns>
        public static MethodInfo GetMethod(this IExecutionContext ctx, Type t, string methodName)
        {
            return GetMethod(ctx, t, methodName, true);
        }

        /// <summary>
        /// Get the named method from a type
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="t"></param>
        /// <param name="methodName"></param>
        /// <param name="throwOnError"></param>
        /// <returns></returns>
        public static MethodInfo GetMethod(this IExecutionContext ctx, Type t, string methodName, bool throwOnError)
        {
            MethodInfo method = ctx.MethodsFor(t, methodName).FirstOrDefault();

            if (method == null && throwOnError)
            {
                throw new MethodNotFoundException(methodName, t);
            }

            return method;
        }

        /// <summary>
        /// Get the method info for method with fixture data
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="target"></param>
        /// <param name="name"></param>
        /// <param name="numargs"></param>
        /// <returns></returns>
        public static MethodInfo GetMethodWithFixtureData(this IExecutionContext ctx, Type target, string name, int numargs)
        {
            foreach (var methodInfo in ctx.MethodsFor(target, name))
            {
                var args = methodInfo.GetParameters();
                int numMethArgs = args.Length;
                if (numMethArgs == numargs && args.Any(p => p.ParameterType == typeof(FixtureData))) 
                    return methodInfo;
            }

            return null;
        }
    }
}
